/**
 *  @file HttpClient.cpp
 */
#include "HttpClient.h"
#include "HttpRequest.h"
#include "HttpResponse.h"
#include "HttpException.h"
#include "message/IHttpBody.h"
#include "../Socket.h"
#include "../SocketFactory.h"
#include "../../collections/BinBuf.h"
#include "../../collections/KVPairT.h"
#include "../../exceptions/BaseException.h"
#include "../../lang/enumT.h"
#include "../../convert/Convert.h"
#include "../../io/BufferedReadStream.h"
#include "../SocketStream.h"
#include "../../os/debug/Debug.h"

using namespace cppflib::net::http::message;
using namespace cppflib::exceptions;
using namespace cppflib::collections;
using namespace cppflib::lang;
using namespace cppflib::io;
using namespace cppflib::os;

namespace cppflib
{

namespace net
{

namespace http
{

/**
 *  Class to get responses from multiple requests
 */
class PipelinedResponses : public IPipelinedResponses
{
private:
   HttpClient *pHttpClient;
   Stream *pReadStream;
   sz_t nrResponse;

private:
   PipelinedResponses() 
   { 
      this->pHttpClient = NULL;
      this->pReadStream = NULL;
      this->nrResponse = 0;
   }

public:
   PipelinedResponses(HttpClient *pHttpClient, sz_t nrResponse) 
   { 
      this->pHttpClient = pHttpClient;
      cppmempool::Retain(this->pHttpClient);

      this->nrResponse = nrResponse;
      this->pReadStream = new BufferedReadStream(
                            new SocketStream(&pHttpClient->GetSock(), pHttpClient->pParams->GetIntParam(HttpParams::P_SOTIMEOUT)), 
                            HttpEndPoint::STREAMBUFFERSIZE);
      cppmempool::Retain(this->pReadStream);
   }

   virtual ~PipelinedResponses()
   {
      cppmempool::Release(pHttpClient);
      cppmempool::Release(pReadStream);
   }

   virtual HttpResponse * GetResponse()
   {
      if (nrResponse == 0)
         return NULL;
      
      --nrResponse;
      return pHttpClient->GetResponse(pReadStream);
   }
};  // end of PipelinedResponses


/*************************** HttpClient *********************************/

HttpClient::HttpClient(void)
{
}

HttpClient::HttpClient(HttpParams * pParams) : HttpEndPoint(pParams)
{
   pRequest = NULL;
   pParams->SetParam(HttpParams::P_ISSERVER, false);
}

HttpClient::~HttpClient(void)
{
   if (this->pRequest)
      cppmempool::Release(this->pRequest);
}

/**
 *  Release previous request if any.
 *  Close connection, too, if it is already closed by peer
 */
void HttpClient::ReplacePreviousRequest(HttpRequest *pNewReq)
{
   cppmempool::Retain(pNewReq);

   if (this->pRequest) {  // previous request
      if (! pNewReq->IsSameHostAndPort(*this->pRequest))
         RemoveSocket(true);

      cppmempool::Release(this->pRequest);
      this->pRequest = NULL;
   }
   
   this->pRequest = pNewReq;
      
   if (IsSocketValid()) {
      if (GetSock().IsClosedByPeer())  // socket may have been closed by peer
         RemoveSocket(true);
   }
   
}

PRIVATE void __PerformBasicChecking(HttpRequest *pReq)
{
   if (!HttpMessage::CanRequestHaveBody(pReq->GetMethod())) {
      if (pReq->HasHttpBody())
         throw HttpException(_S("This request shouldn't contain a body"));

      if (pReq->ExpectContinueExists())
         throw HttpException(_S("This request shouldn't contain a \"Expect: 100-continue\" header"));
   }

}


/**
 *  Issue a request to server
 *
 *  @note exception is thrown if timeout
 *
 *  @return a response if server has at least responded with a complete header
 */
HttpResponse * HttpClient::IssueRequest(HttpRequest *pNewReq)
{   
   if (!pNewReq) 
      throw HttpException(_S("Request is NULL"));

   ReplacePreviousRequest(pNewReq);  // remove previous request if any
   __PerformBasicChecking(pRequest);

   try {
      
      if (!IsSocketValid()) {  
         InitSocket();     // init a new socket
         ConnectServer();
      }
      InitDefaultHeaders(*pRequest);
      SendRequest();
      HttpResponse *pResponse = GetResponse();
      CloseConnectionIfRequired(*pResponse);
      return pResponse;
   }
   catch (BaseException &e) {
      RemoveSocket(true); // close opened socket on error
      throw e;          // throw again
   }

   return NULL;
}

/**
 *  Issue pipelined (multiple) requests at the same time
 *
 *  @param [in] requests - array of requests
 *
 *  @return IPipelinedResponses to get the responses one by one
 */
IPipelinedResponses * HttpClient::IssuePipelinedRequests(ArrayCppObjT<HttpRequest> & requests)
{
   for (int i = 0 ; i < static_cast<int>(requests.GetSize()); ++i) {
      HttpRequest *pReq = requests.Get(i);
      if (pReq->GetMethod() != http::GET && pReq->GetMethod() != http::HEAD)
         throw HttpException(_S("Only GET and HEAD methods can be pipelined"));
      __PerformBasicChecking(pReq);
   }


   try {
      for (int i = 0 ; i < static_cast<int>(requests.GetSize()); ++i) {
         HttpRequest *pReq = requests.Get(i);
         ReplacePreviousRequest(pReq);

         if (!IsSocketValid()) {  
            if (i > 0)
               throw HttpException(_S("Failed in issuing multiple requests simultaneously"));
            InitSocket();     // init a new socket
            ConnectServer();
         }

         InitDefaultHeaders(*pReq);
         SendRequest();
      }
      
      return new PipelinedResponses(this, requests.GetSize());
   }
   catch (BaseException &e) {
      RemoveSocket(true); // close opened socket on error
      throw e;          // throw again
   }

   return NULL;
}

/**
 *  Init default headers if not set by user
 */
void HttpClient::InitDefaultHeaders(HttpMessage &httpMsg)
{
   CFString h(_S("Host"));     // this is required by HTTP/1.1
   if (!httpMsg.HeaderExists(h)) {
      CFString host;
      HttpRequest *pRequest = static_cast<HttpRequest*>(&httpMsg);
      pRequest->GetHost(host);
      int port = pRequest->GetPort();
      if (( !pRequest->IsSSLRequired() && port != http::HTTP_PORT) ||
           ( pRequest->IsSSLRequired() && port != http::HTTPS_PORT))
         host.AppendFormat(_S(":%d"), port);  // append port, too, if not using standard one
      pRequest->SetHeader(h, host);
   }

   h = _S("User-Agent");
   if (!httpMsg.HeaderExists(h)) {
      const CFString *pUserAgent = pParams->GetStrParam(HttpParams::P_USERAGENT);
      httpMsg.SetHeader(h, *pUserAgent);
   }

   h = _S("Accept");
   if (!httpMsg.HeaderExists(h)) {
      httpMsg.SetHeader(h, _S("*/*"));
   }

   h = _S("Accept-Encoding");
   if (!httpMsg.HeaderExists(h)) {
      httpMsg.SetHeader(h, _S("gzip, deflate"));
   }

   HttpEndPoint::InitDefaultHeaders(httpMsg); // call base class, too
   
   //
   // possible extra headers
   //
   // Proxy-Connection: Keep-Alive 
   // Connection: Keep-Alive 
}

/**
 *  Init client socket from socket factory
 */
void HttpClient::InitSocket()
{
   if (!pParams->Exists(HttpParams::P_SOCKETFACTORY)) {
      if (!pRequest->IsSSLRequired())  // create a default socket factory if SSL isn't required
         pParams->SetParam(HttpParams::P_SOCKETFACTORY, new SocketFactory());
      else 
         throw HttpException(_S("Socket factory not present"));
   }

   SocketFactory *pSockFactory = static_cast<SocketFactory*>(const_cast<BaseObject*>(
                                                                    pParams->GetParam(HttpParams::P_SOCKETFACTORY)));
   Socket *pSocket = pSockFactory->CreateSocket(pRequest->IsSSLRequired(), CFString::Empty(), 0);
   if (!pSocket)
      throw HttpException(_S("Create socket failed"));

   AssignSocket(pSocket);
}

/**
 *  Helper class to swap between 2 HTTP requests
 */
class HttpRequestSwapper
{
private:
   HttpRequest *pOrgRequest;
   HttpRequest **pAddrOfOrgRequest;

public:
   HttpRequestSwapper(HttpRequest **ppOrgRequest, HttpRequest * pNewRequest)
   {
      pAddrOfOrgRequest = ppOrgRequest;
      pOrgRequest = *pAddrOfOrgRequest;
      *pAddrOfOrgRequest = pNewRequest;  // point to new request
   }

   ~HttpRequestSwapper()
   {
      *pAddrOfOrgRequest = pOrgRequest;  // swap back
   }

};

/**
 *  Establish a SSL tunnel through proxy by 'CONNECT' method
 */
void HttpClient::EstablishProxySSLTunnel()
{
   cppmempool::MemPoolWrap localPool;

   // prepare a CONNECT request to be sent to proxy
   CFString url;
   this->pRequest->GetURLForProxy(url);
   HttpRequest *pProxyReq = new HttpRequest(http::CONNECT);
   pProxyReq->SetUrl(url);
   pProxyReq->SetHeader(_S("Content-Length"), convert::ToString(0));
   pProxyReq->SetHeader(_S("Proxy-Connection"), _S("Keep-Alive"));

   // turn off SSL temporarily (need to communicate with proxy in plain text)
   GetSock().SetAttribute(Socket::SSL_SENDRECV_WITHSSL, Boolean(false));

   // connect to proxy
   const NameValuePair *pProxyInfo = static_cast<const NameValuePair *>(pParams->GetParam(HttpParams::P_PROXY));
   int connPort = convert::ToInt32(*pProxyInfo->GetVal());
   if (!GetSock().Connect(*pProxyInfo->GetKey(), connPort, pParams->GetIntParam(HttpParams::P_CONNTIMEOUT)))
      throw HttpException(_S("Connect to proxy failed"));

   // swap 2 requests (will be recovered automatically upon function exiting)
   HttpRequestSwapper swapper(&this->pRequest, pProxyReq);
   InitDefaultHeaders(*pProxyReq);
   SendRequest();
   HttpResponse *pResponse = GetResponse();
   if (pResponse->GetResponseCode() != http::ST_OK) {
      CFString err;
      err.Format(_S("Failed to establish SSL connecton, res code = %d"), pResponse->GetResponseCode());
      throw HttpException(err);
   }

   //
   // Turn on again if SSL connection is established.
   // This socket is closed if exception occurs, thus, it doesn't matter that this socket's SSL feature 
   // must be recovered when function returns unconditionally
   // 
   GetSock().SetAttribute(Socket::SSL_SENDRECV_WITHSSL, Boolean(true));
}

/**
 *  Connect to target server
 */
void HttpClient::ConnectServer()
{
   CFString targetHost;
   pRequest->GetHost(targetHost);
   int connPort = pRequest->GetPort();

   if (pParams->Exists(HttpParams::P_PROXY)) {
      
      if (pRequest->IsSSLRequired()) {
         EstablishProxySSLTunnel();
      }
      else {
         const NameValuePair *pProxyInfo = static_cast<const NameValuePair *>(pParams->GetParam(HttpParams::P_PROXY));
         targetHost = *pProxyInfo->GetKey();
         connPort = convert::ToInt32(*pProxyInfo->GetVal());
      }
   }
   
   if (!GetSock().Connect(targetHost, connPort, pParams->GetIntParam(HttpParams::P_CONNTIMEOUT)))
      throw HttpException(_S("Connect to server failed"));
}

/**
 *  Send request line, headers and body (if any)
 */
void HttpClient::SendRequest()
{
   SendRequestLine();
   SendHeaders(*pRequest);
   if (pRequest->IsVersion1Point1() && pRequest->HasHttpBody() && pRequest->ExpectContinueExists()) { 
      // HTTP 1.1 client expect a "Continue" from server
      HttpResponse *pResponse = GetResponse();
      int code = pResponse->GetResponseCode();
      cppmempool::Release(pResponse);
      if (code != http::ST_CONTINUE)
         throw HttpException(_S("Server has rejected request"));
   }

   SendBody(*pRequest);
}

/**
 *  Send request line
 */
void HttpClient::SendRequestLine()
{
   CFString reqPath;
   if (pRequest->GetMethod() == http::CONNECT) {
      pRequest->GetHost(reqPath);
      reqPath.AppendFormat(_S(":%d"), pRequest->GetPort());
   }
   else if (pParams->Exists(HttpParams::P_PROXY) && ! pRequest->IsSSLRequired()) {
      pRequest->GetURLForProxy(reqPath);
   }
   else {
      pRequest->GetReqPath(reqPath, true);
   }
   
   CFString reqLine;
   reqLine.Format(_S("%s %s HTTP/%s\r\n"), 
                     Enum::GetString<http::httpmethod_t>(pRequest->GetMethod()),
                     static_cast<pcwstr_t>(reqPath), 
                     static_cast<pcwstr_t>(pRequest->GetHttpVersion()));

//cppflib::os::debug::PlainPrint(reqLine);

   if (!SendString(GetSock(), reqLine))
      throw HttpException(_S("Send request line error"));
}


/**
 *  Parse respsonse status line
 *  
 *  @note Headers fetched is saved in HttpEndPoint::pTempBuf
 */
HttpResponse * HttpClient::ParseStatusLine()
{
   do {
      CFString statusLine;
      if (!GetOneLine(statusLine)) 
         break;

      int endPos;
      CFString ver;
      if (!__GetHttpVersion(statusLine, ver, endPos))   // get version
         break;

      statusLine.Delete(0, endPos);
      statusLine.TrimLeft();

      int resCode = convert::ToInt32(statusLine);    // get status code
      if (resCode <= 0)
         break;

      CFString reason;                               // get reason phrase (may not present)
      int pos = statusLine.Find(_S(' '));
      if (pos >= 0) {
         statusLine.Delete(0, pos);
         statusLine.TrimLeft();
         reason = statusLine;
      }

      return new HttpResponse(ver, resCode, reason);

   } while(false);

   throw HttpException(_S("Invalid status line"), http::InvalidStatusLine);
   return NULL;
}



/**
 *  Receive response. This function is returned when at least all headers
 *  are retrieved.
 *
 *  @param [in] pReadStream -- NULL if a single request/response pair;
 *                             non-NULL if it is called from a PipelinedResponses object
 */
HttpResponse * HttpClient::GetResponse(Stream * pReadStream)
{
   Stream *pStream = pReadStream;

   if (!pStream)
      pStream = new BufferedReadStream(
                       new SocketStream(&GetSock(), pParams->GetIntParam(HttpParams::P_SOTIMEOUT)), 
                       HttpEndPoint::STREAMBUFFERSIZE);
   GetHeaders(*pStream);
   HttpResponse *pResponse = ParseStatusLine();
   ParseHeaders(*pResponse);

   
   if (HttpMessage::ResponseCodeHaveNoBody(pResponse->GetResponseCode()) ||
       ! HttpMessage::CanResponseHaveBody(pRequest->GetMethod())) {
      if (pReadStream == NULL)
         cppmempool::Release(pStream); // no longer use (release only if it is created by this function)
   }
   else {
      // init body
      CFString *pVal = NULL;
      u64_t contentLength = (pResponse->GetHeaderValue(CFString("Content-Length"), pVal)) ? 
                                      convert::ToInt64(static_cast<pcwstr_t>(*pVal)) : 0;

      pResponse->SetHttpBody(new HttpPeerBody(pStream, contentLength, 
                                              pResponse->ChunkTransferEncodingExists(),
                                              pResponse->CompressedContentExists()));
   }

   return pResponse;
}

/**
 *  Peer may request to close connection after responding
 */
void HttpClient::CloseConnectionIfRequired(HttpResponse &res)
{
   if (res.HeaderValueExists(CFString(_S("Connection")), CFString(_S("close"))) ||
       res.HeaderValueExists(CFString(_S("Proxy-Connection")), CFString(_S("close"))) )
      RemoveSocket(false); // HttpPeerBody would have a ref on it, so it won't be released immediately
}



} // end of namespace http

} // end of namespace net

} // end of namespace cppflib
